/* Files */
AddCSLuaFile( "cl_init.lua" )
AddCSLuaFile( "shared.lua" )

/* Includes */
include('shared.lua')

/*************************************************************************************************************/
/**  CONSTANTS																								**/
/*************************************************************************************************************/

/* Define per material configuration */
local mats = {
	[MAT_FOLIAGE]			= { Name = "MAT_FOLIAGE", Friction = 0.9, Bounce = 0, Penetration = 0.1, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_ALIENFLESH]		= { Name = "MAT_ALIENFLESH", Friction = 0.8, Bounce = 0.1, Penetration = 0.2, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_ANTLION]			= { Name = "MAT_ANTLION", Friction = 0.7, Bounce = 0.2, Penetration = 0.25, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "", SpillBlood = true, ShakeFactor = 0.75 },
	[MAT_BLOODYFLESH]		= { Name = "MAT_BLOODYFLESH", Friction = 0.8, Bounce = 0.1, Penetration = 0.2, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_FLESH]				= { Name = "MAT_FLESH", Friction = 0.8, Bounce = 0.1, Penetration = 0.2, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "", SpillBlood = true, ShakeFactor = 0.5 },
	[45]					= { Name = "MAT_METROCOP_HEAD", Friction = 0.7, Bounce = 0.1, Penetration = 0.15, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_DIRT]				= { Name = "MAT_DIRT", Friction = 0.7, Bounce = 0, Penetration = 0.15, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_WOOD]				= { Name = "MAT_WOOD", Friction = 0.5, Bounce = 0.3, Penetration = 0.25, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 1.5 },
	[MAT_SAND]				= { Name = "MAT_SAND", Friction = 0.7, Bounce = 0, Penetration = 0.15, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_GLASS]				= { Name = "MAT_GLASS", Friction = 0.6, Bounce = 0.4, Penetration = 0.35, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 2 },
	[MAT_CLIP]				= { Name = "MAT_CLIP", Friction = 1, Bounce = 0, Penetration = 0.0, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = false, ShakeFactor = 0 },
	[MAT_COMPUTER]			= { Name = "MAT_COMPUTER", Friction = 0.3, Bounce = 0.5, Penetration = 0.4, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 1 },
	[MAT_PLASTIC]			= { Name = "MAT_PLASTIC", Friction = 0.4, Bounce = 0.8, Penetration = 0.4, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 1 },
	[MAT_TILE]				= { Name = "MAT_TILE", Friction = 0.3, Bounce = 0.25, Penetration = 0.6, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 1 },
	[MAT_CONCRETE]			= { Name = "MAT_CONCRETE", Friction = 0.1, Bounce = 0.4, Penetration = 0.85, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 2 },
	[MAT_GRATE]				= { Name = "MAT_GRATE", Friction = 0.8, Bounce = 0, Penetration = 0.15, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_VENT]				= { Name = "MAT_VENT", Friction = 0.8, Bounce = 0, Penetration = 0.2, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 0.5 },
	[MAT_METAL]				= { Name = "MAT_METAL", Friction = 0.4, Bounce = 0.9, Penetration = 0.6, EffSize = 1, EffType = "gdcw_universal_impact", Decal = "ExplosiveGunshot", SpillBlood = true, ShakeFactor = 3 },
	[MAT_SLOSH]				= { Name = "MAT_SLOSH", Friction = 0.7, Bounce = 0, Penetration = 0.15, EffSize = 15, EffType = "watersplash", Decal = "", SpillBlood = false, ShakeFactor = 0 }
}

/*************************************************************************************************************/
/**  CONFIGURATION																							**/
/*************************************************************************************************************/

/* Configuration */
ENT.MaxVelocity		= 430											-- Maximum velocity in m / s
ENT.MinVelocity		= 1												-- If it goes below that speed it will be removed
ENT.Penetration		= 100											-- How deep it goes
ENT.BounceFactor	= 1												-- How much it bounces on things
ENT.EffectSize		= 1.2											-- How much stuff gets kicked up on impact
ENT.TissueDamage	= 60											-- Player damage is multiplied by 2 for some reason
ENT.BallisticDrag	= 200											-- Fraction of velocity lost per tick, higher is less
ENT.Drift			= 20											-- How much the bullet will drift in flight (Inaccuracy)
ENT.MaxDistance		= 2500											-- Maximum distance it can reach (at max velocity) before being removed
ENT.Weight			= 0.013											-- Weight of bullet in kg (13 grams = 200 grains)

/*************************************************************************************************************/
/**  HELPERS																								**/
/*************************************************************************************************************/

/* Gets the length of a vector */
function ENT:GetVecLength( vec )
	return math.sqrt( math.pow( vec.x, 2 ) + math.pow( vec.y, 2 ) + math.pow( vec.z, 2 ) )
end

/* Gets the reflection vector */
function ENT:ReflectVec( vec, normal )
	return 2 * ( normal:Dot( -vec ) ) * normal + vec
end

/* Gets the angle between a vector and a normal */
function ENT:HitAngle( vec, normal )
	local dot		= vec:Dot( normal )
	local mul		= vec:Length() * normal:Length()
	return			 90 - ( math.deg( math.acos( dot / mul ) ) - 90 )
end

/* Safely remove the entity */
function ENT:SafeRemove()
	if IsValid( self.Entity ) then
		self.Entity:Remove()
	end
	return true
end

/* Calculate speed ratio */
function ENT:SpeedRatio()
	return self.Flightvector:Length() / self.MaxVelocity
end

/* Get material type info */
function ENT:GetMatInfo( matTp )
	return mats[ matTp ]
end 

/*************************************************************************************************************/
/**  FUNCTIONS																								**/
/*************************************************************************************************************/

/* Create an effect */
function ENT:CreateEffect( trace )

	/* If too slow, do not create any effect */
	if self.Flightvector:Length() < 10 then
		MsgN( "Too slow, no effects" )
		return false
	end
	
	/* If bouncing, do not make effects */
	if self.Bouncing and self.Flightvector:Length() < 50 then
		MsgN( "Bouncing, no effects - Speed: " .. tostring( self.Flightvector:Length() ) )
		return false
	end

	/* Variables */
	local matInfo		= self:GetMatInfo( trace.MatType )
	
	/* If we're inside something, do not make any effect */
	if trace.StartSolid and not trace.HitWater then
		return false
	end
	MsgN( "Speed ratio: " .. tostring( self:SpeedRatio() ) )
	/* Effect data */
	local effectdata	= EffectData()
	effectdata:SetOrigin( trace.HitPos )
	effectdata:SetNormal( trace.HitNormal )
	effectdata:SetStart( self.Flightvector:GetNormalized() )
	effectdata:SetScale( matInfo.EffSize * self.EffectSize )
	effectdata:SetRadius( trace.MatType )
	
	/* Create the effect */
	util.Effect( matInfo.EffType, effectdata )
	
	/* Add a blood splat if needed */
	if self.HitFlesh and matInfo.SpillBlood then
		util.Decal( "blood", trace.HitPos + trace.HitNormal, trace.HitPos - trace.HitNormal )
	end
	
	/* Add a bullet hole if needed */
	if matInfo.Decal then
		util.Decal( matInfo.Decal, trace.HitPos + trace.HitNormal, trace.HitPos - trace.HitNormal)
	end
	
	/* Shake the screen at hit pos */
	local shake		= matInfo.ShakeFactor
	util.ScreenShake( trace.HitPos, shake * 10, shake * 5, shake * 0.2, shake * 200 )
	
	return true
end

/* Apply damage to victim */
function ENT:ApplyDamage( trace )

	/* Prepare damage info */
	local dmginfo	= DamageInfo()
	local dmg		= math.Rand( self.TissueDamage / 2, self.TissueDamage ) * ( self.Flightvector:Length() / self.MaxVelocity )
	MsgN( "Damage: " .. tostring( dmg ) )
	dmginfo:SetDamage( dmg )
	dmginfo:SetDamageType( DMG_BULLET )						-- Bullet damage
	dmginfo:SetAttacker( self.Owner )						-- Shooter gets credit
	dmginfo:SetInflictor( self.Entity )						-- Bullet gets credit
	dmginfo:SetDamageForce( self.Flightvector:GetNormalized() * self.EffectSize * 3 )	-- A little thump...
	
	/* Based on hit group */
	hitgroup = trace.HitGroup
	if		hitgroup == HITGROUP_GENERIC		then dmginfo:ScaleDamage( 1 )
	elseif	hitgroup == HITGROUP_STOMACH		then dmginfo:ScaleDamage( 1 )
	elseif	hitgroup == HITGROUP_GEAR			then dmginfo:ScaleDamage( 1 )
	elseif	hitgroup == HITGROUP_CHEST			then dmginfo:ScaleDamage( math.Rand( 1, 1.5 ) )
	elseif	hitgroup == HITGROUP_LEFTARM		then dmginfo:ScaleDamage( math.Rand( 0.3, 0.5 ) )
	elseif	hitgroup == HITGROUP_RIGHTARM		then dmginfo:ScaleDamage( math.Rand( 0.3, 0.5 ) )
	elseif	hitgroup == HITGROUP_LEFTLEG		then dmginfo:ScaleDamage( math.Rand( 0.3, 0.6 ) )
	elseif	hitgroup == HITGROUP_RIGHTLEG		then dmginfo:ScaleDamage( math.Rand( 0.3, 0.6 ) )
	elseif	hitgroup == HITGROUP_HEAD			then dmginfo:ScaleDamage( 5 )
	else	dmginfo:ScaleDamage( 1 ) 			end
	
	/* Apply damage */
	trace.Entity:TakeDamageInfo( dmginfo )
end

/* Deal with bullet penetration */
function ENT:Penetrate( trace )

	/* Variables */
	local matInfo		= self:GetMatInfo( trace.MatType )
	local newVec		= self.Flightvector

	/* Apply heavy speed reduction (percent of current) */
	if not trace.HitWater then
		newVec	= self.Flightvector * matInfo.Friction
		
	/* Inside of water, just apply some water friction */
	else
		MsgN( "Under water PEN..." )
		local a				= 9.81 * ( -matInfo.Friction )
		newVecl	= self.Flightvector + self.Flightvector:GetNormalized() * a
	end
	
	/* Do not penetrate if the bullet's speed has been reduced too much */
	if newVec:Length() < matInfo.Penetration * self.Penetration then
		if trace.HitWater then
			MsgN( "Too slow for water" )
		end
		return false, newVec
	end
	return true, newVec
end

/* Deal with bullet bouncing */
function ENT:Bounce( trace )
	
	/* Variables */
	local matInfo		= self:GetMatInfo( trace.MatType )
	local rDir			= self:ReflectVec( self.Flightvector, trace.HitNormal )					-- calculate reflected direction of the bullet trace
	local hitAngle		= self:HitAngle( self.Flightvector, trace.HitNormal )					-- calculate angle between bullet trace and trace normal
	local radAngle		= math.rad( hitAngle ) / math.rad( 90 )									-- keep it between 0 and 1 to have a percent value
	local bounce		= rDir * self.BounceFactor * matInfo.Bounce * radAngle					-- final bounce value
	
	/* If bouncing is too low, do nothing */
	MsgN( "Rad angle: " .. tostring( radAngle ) )
	MsgN( "Normal len: " .. tostring( trace.HitNormal:Length() ) .. " - Vec len: " .. tostring( self.Flightvector:Length() ) )
	MsgN( "Hit angle: " .. tostring( hitAngle ) .. " - Bounce: " .. tostring( bounce ) )
	
	/* Do not bounce if too slow */
	if bounce:Length() < 0.1 then return false end
	
	/* Update flight vector */
	self.Flightvector	= bounce + ( VectorRand():GetNormalized() / 10 )						-- slightly randomize the final bullet bounce direction
	
	return true
end

/*************************************************************************************************************/
/**  HOOKS																									**/
/*************************************************************************************************************/

/* Initialization */
function ENT:Initialize()

	math.randomseed( CurTime() )
	self.Owner 			= self:GetOwner()
	
	/* Initialize */
	self.Flightvector 	= self.Entity:GetUp() * ( ( self.MaxVelocity * 52.5 ) / 66)	-- To where and how fast it starts
	self.Timeout 		= CurTime() + ( self.MaxDistance / self.MaxVelocity ) * 1.5	-- How long before auto-remove
	self.HitFlesh		= false

	/* Entity properties */
	self.Entity:SetModel( "models/ball.mdl" )
	self.Entity:PhysicsInit( SOLID_VPHYSICS )
	self.Entity:SetMoveType( MOVETYPE_NONE )
	self.Entity:SetSolid( SOLID_VPHYSICS )
	
	/* Set up a trace filter (avoid checking owner and self) */
	self.Filter			= { [1] = self.Owner, [2] = self.Entity }
		   
	self:Think()
end   


function ENT:Think()

	/* Remember last think */
	if not self.LastThink then
		self.LastThink			= CurTime()
		self.VertVel			= 0
	end

	/* If there was an error, remove after the max delay or if entity is invalid */
	if self.Timeout < CurTime() or not IsValid( self.Entity ) then
		return self:SafeRemove()
	end
	
	/* Create a trace to the next tick position */
	local traceData 		= {}
	traceData.start 		= self.Entity:GetPos()
	traceData.endpos 		= self.Entity:GetPos() + self.Flightvector
	traceData.filter 		= self.Filter
	traceData.mask			= MASK_SHOT + MASK_WATER
	local trace = util.TraceLine( traceData )
	
	/* If it hit something */
	if trace.Hit then
	
		/* Variables */
		local matInfo		= self:GetMatInfo( trace.MatType )
	
		/* Delete it if it hits the sky */
		if trace.HitSky then
			return self:SafeRemove()
		end
		
		/* Check for water */
		if trace.StartSolid then			
			
			/* Inside water */
			if ( util.PointContents( trace.HitPos ) & CONTENTS_WATER) == CONTENTS_WATER then
				MsgN( "Hits water" )
				traceData.mask		= MASK_SHOT
				trace				= util.TraceLine( traceData )
				if not trace.Hit then
					MsgN( "Remained under water" )
					trace.MatType	= MAT_SLOSH
				end
				trace.HitWater	= true
			end
		end
	
		/* If an entity has been hit */
		if IsValid( trace.Entity ) then
		
			/* Tell to show blood further on if we hit a player or NPC */
			if trace.Entity:IsPlayer() or trace.Entity:IsNPC() then
				self.HitFlesh = true
			end
			
			/* Apply damage to victim */
			self:ApplyDamage( trace )
		end
		
		/* Apply penetration */
		local pen, newVec = self:Penetrate( trace )
		if not pen then
		
			/* Remove if inside a wall or something */
			if trace.StartSolid then
				if not trace.HitWater then
					MsgN( "Inside wall" )
					self:SafeRemove()
					
				/* If under water, just apply the new velocity */
				else
					self.Flightvector	= newVec
					MsgN( "Inside water vel: " .. tostring( self.Flightvector ) )
				end
				
			/* Apply bounce */
			else
				self.Bouncing = self:Bounce( trace )
				if not self.Bouncing then
					MsgN( "Couldn't bounce" )
					self:SafeRemove()
				else
					self.VertVel = -self.VertVel * self.BounceFactor * matInfo.Bounce
				end
			end
			
		/* Otherwise apply the new velocity */
		else
			self.Flightvector	= newVec
			if trace.HitWater then
				MsgN( "Pen under water at " .. tostring( newVec:Length() ) )
				if trace.Hit then
					MsgN( "Hit wall under water" )
				end
			end
		end
	
		/* Create effects & decals */
		self:CreateEffect( trace )
	end
	
	/* If the entity is no longer valid, don't do anything else */
	if not IsValid( self.Entity ) then
		self:SafeRemove()
		return true
	end
	
	/* Decrease speed & drift */
	self.Flightvector = self.Flightvector - self.Flightvector / self.BallisticDrag
	
	/* Apply gravity */
	self.VertVel		= self.VertVel - 9.81 * ( CurTime() - self.LastThink ) * self.Weight
	self.Flightvector	= self.Flightvector + Vector( 0, 0, self.VertVel )
	
	/* Move the bullet */
	self.Entity:SetPos( self.Entity:GetPos() + self.Flightvector )
	
	/* Update angles */
	self.Entity:SetAngles( self.Flightvector:Angle() + Angle( 90, 0,0 ) )
	
	/* Next think now */
	self.Entity:NextThink( CurTime() )
	
	return true
end
